/* * $Id: 4fcfb2a0cf2932be5fc1604e52b70d91ec989252 $ * * This file is part of the iText (R) project. * Copyright (c) 1998-2016 iText Group NV * Authors: Bruno Lowagie, Paulo Soares, et al. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License version 3 * as published by the Free Software Foundation with the addition of the * following permission added to Section 15 as permitted in Section 7(a): * FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY * ITEXT GROUP. ITEXT GROUP DISCLAIMS THE WARRANTY OF NON INFRINGEMENT * OF THIRD PARTY RIGHTS * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Affero General Public License for more details. * You should have received a copy of the GNU Affero General Public License * along with this program; if not, see http://www.gnu.org/licenses or write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA, 02110-1301 USA, or download the license from the following URL: * http://itextpdf.com/terms-of-use/ * * The interactive user interfaces in modified source and object code versions * of this program must display Appropriate Legal Notices, as required under * Section 5 of the GNU Affero General Public License. * * In accordance with Section 7(b) of the GNU Affero General Public License, * a covered work must retain the producer line in every PDF that is created * or manipulated using iText. * * You can be released from the requirements of the license by purchasing * a commercial license. Buying such a license is mandatory as soon as you * develop commercial activities involving the iText software without * disclosing the source code of your own applications. * These activities include: offering paid services to customers as an ASP, * serving PDFs on the fly in a web application, shipping iText with a closed * source product. * * For more information, please contact iText Software Corp. at this * address: sales@itextpdf.com */ package com.itextpdf.text.pdf.pdfcleanup; import com.itextpdf.awt.geom.AffineTransform; import com.itextpdf.awt.geom.NoninvertibleTransformException; import com.itextpdf.awt.geom.Point; import com.itextpdf.awt.geom.Point2D; import com.itextpdf.text.Rectangle; import com.itextpdf.text.pdf.PdfContentByte; import com.itextpdf.text.pdf.parser.*; import com.itextpdf.text.pdf.parser.Path; import com.itextpdf.text.pdf.parser.clipper.*; import com.itextpdf.text.pdf.parser.clipper.Clipper.*; import com.itextpdf.text.pdf.parser.clipper.Point.LongPoint; import java.util.*; class PdfCleanUpRegionFilter extends RenderFilter { private List<Rectangle> rectangles; private static final double circleApproximationConst = 0.55191502449; public PdfCleanUpRegionFilter(List<Rectangle> rectangles) { this.rectangles = rectangles; } /** * Checks if the text is inside render filter region. */ @Override public boolean allowText(TextRenderInfo renderInfo) { LineSegment ascent = renderInfo.getAscentLine(); LineSegment descent = renderInfo.getDescentLine(); Point2D[] glyphRect = new Point2D[] { new Point2D.Float(ascent.getStartPoint().get(0), ascent.getStartPoint().get(1)), new Point2D.Float(ascent.getEndPoint().get(0), ascent.getEndPoint().get(1)), new Point2D.Float(descent.getEndPoint().get(0), descent.getEndPoint().get(1)), new Point2D.Float(descent.getStartPoint().get(0), descent.getStartPoint().get(1)), }; for (Rectangle rectangle : rectangles) { Point2D[] redactRect = getVertices(rectangle); if (intersect(glyphRect, redactRect)) { return false; } } return true; } @Override public boolean allowImage(ImageRenderInfo renderInfo) { throw new UnsupportedOperationException(); } /** * Calculates intersection of the image and the render filter region in the coordinate system relative to the image. * * @return <code>null</code> if the image is not allowed, {@link java.util.List} of * {@link com.itextpdf.text.Rectangle} objects otherwise. */ protected List<Rectangle> getCoveredAreas(ImageRenderInfo renderInfo) { Rectangle imageRect = calcImageRect(renderInfo); List<Rectangle> coveredAreas = new ArrayList<Rectangle>(); if (imageRect == null) { return null; } for (Rectangle rectangle : rectangles) { Rectangle intersectionRect = intersection(imageRect, rectangle); if (intersectionRect != null) { // True if the image is completely covered if (imageRect.equals(intersectionRect)) { return null; } coveredAreas.add(transformIntersection(renderInfo.getImageCTM(), intersectionRect)); } } return coveredAreas; } protected Path filterStrokePath(Path sourcePath, Matrix ctm, float lineWidth, int lineCapStyle, int lineJoinStyle, float miterLimit, LineDashPattern lineDashPattern) { Path path = sourcePath; JoinType joinType = getJoinType(lineJoinStyle); EndType endType = getEndType(lineCapStyle); if (lineDashPattern != null) { if (!lineDashPattern.isSolid()) { path = applyDashPattern(path, lineDashPattern); } } ClipperOffset offset = new ClipperOffset(miterLimit, PdfCleanUpProcessor.arcTolerance * PdfCleanUpProcessor.floatMultiplier); List<Subpath> degenerateSubpaths = addPath(offset, path, joinType, endType); PolyTree resultTree = new PolyTree(); offset.execute(resultTree, lineWidth * PdfCleanUpProcessor.floatMultiplier / 2); Path offsetedPath = convertToPath(resultTree); if (degenerateSubpaths.size() > 0) { if (endType == EndType.OPEN_ROUND) { List<Subpath> circles = convertToCircles(degenerateSubpaths, lineWidth / 2); offsetedPath.addSubpaths(circles); } else if (endType == EndType.OPEN_SQUARE && lineDashPattern != null) { List<Subpath> squares = convertToSquares(degenerateSubpaths, lineWidth, sourcePath); offsetedPath.addSubpaths(squares); } } return filterFillPath(offsetedPath, ctm, PathPaintingRenderInfo.NONZERO_WINDING_RULE); } /** * Note: this method will close all unclosed subpaths of the passed path. * * @param fillingRule If the subpath is contour, pass any value. */ protected Path filterFillPath(Path path, Matrix ctm, int fillingRule) { path.closeAllSubpaths(); Clipper clipper = new DefaultClipper(); addPath(clipper, path); for (Rectangle rectangle : rectangles) { Point2D[] transfRectVertices = transformPoints(ctm, true, getVertices(rectangle)); addRect(clipper, transfRectVertices, PolyType.CLIP); } PolyFillType fillType = PolyFillType.NON_ZERO; if (fillingRule == PathPaintingRenderInfo.EVEN_ODD_RULE) { fillType = PolyFillType.EVEN_ODD; } PolyTree resultTree = new PolyTree(); clipper.execute(ClipType.DIFFERENCE, resultTree, fillType, PolyFillType.NON_ZERO); return convertToPath(resultTree); } private static JoinType getJoinType(int lineJoinStyle) { switch (lineJoinStyle) { case PdfContentByte.LINE_JOIN_BEVEL: return JoinType.BEVEL; case PdfContentByte.LINE_JOIN_MITER: return JoinType.MITER; } return JoinType.ROUND; } private static EndType getEndType(int lineCapStyle) { switch (lineCapStyle) { case PdfContentByte.LINE_CAP_BUTT: return EndType.OPEN_BUTT; case PdfContentByte.LINE_CAP_PROJECTING_SQUARE: return EndType.OPEN_SQUARE; } return EndType.OPEN_ROUND; } /** * Converts specified degenerate subpaths to circles. * Note: actually the resultant subpaths are not real circles but approximated. * * @param radius Radius of each constructed circle. * @return {@link java.util.List} consisting of circles constructed on given degenerated subpaths. */ private static List<Subpath> convertToCircles(List<Subpath> degenerateSubpaths, double radius) { List<Subpath> circles = new ArrayList<Subpath>(degenerateSubpaths.size()); for (Subpath subpath : degenerateSubpaths) { BezierCurve[] circleSectors = approximateCircle(subpath.getStartPoint(), radius); Subpath circle = new Subpath(); circle.addSegment(circleSectors[0]); circle.addSegment(circleSectors[1]); circle.addSegment(circleSectors[2]); circle.addSegment(circleSectors[3]); circles.add(circle); } return circles; } /** * Converts specified degenerate subpaths to squares. * Note: the list of degenerate subpaths should contain at least 2 elements. Otherwise * we can't determine the direction which the rotation of each square depends on. * * @param squareWidth Width of each constructed square. * @param sourcePath The path which dash pattern applied to. Needed to calc rotation angle of each square. * @return {@link java.util.List} consisting of squares constructed on given degenerated subpaths. */ private static List<Subpath> convertToSquares(List<Subpath> degenerateSubpaths, double squareWidth, Path sourcePath) { List<Point2D> pathApprox = getPathApproximation(sourcePath); if (pathApprox.size() < 2) { return Collections.EMPTY_LIST; } Iterator<Point2D> approxIter = pathApprox.iterator(); Point2D approxPt1 = approxIter.next(); Point2D approxPt2 = approxIter.next(); StandardLine line = new StandardLine(approxPt1, approxPt2); List<Subpath> squares = new ArrayList<Subpath>(degenerateSubpaths.size()); float widthHalf = (float) squareWidth / 2; for (int i = 0; i < degenerateSubpaths.size(); ++i) { Point2D point = degenerateSubpaths.get(i).getStartPoint(); while (!line.contains(point)) { approxPt1 = approxPt2; approxPt2 = approxIter.next(); line = new StandardLine(approxPt1, approxPt2); } double slope = line.getSlope(); double angle; if (slope != Float.POSITIVE_INFINITY) { angle = Math.atan(slope); } else { angle = Math.PI / 2; } squares.add(constructSquare(point, widthHalf, angle)); } return squares; } private static List<Point2D> getPathApproximation(Path path) { List<Point2D> approx = new ArrayList<Point2D>() { @Override public boolean addAll(Collection<? extends Point2D> c) { Point2D prevPoint = (size() - 1 < 0 ? null : get(size() - 1)); boolean ret = false; for (Point2D pt : c) { if (!pt.equals(prevPoint)) { add(pt); prevPoint = pt; ret = true; } } return true; } }; for (Subpath subpath : path.getSubpaths()) { approx.addAll(subpath.getPiecewiseLinearApproximation()); } return approx; } private static Subpath constructSquare(Point2D squareCenter, double widthHalf, double rotationAngle) { // Orthogonal square is the square with sides parallel to one of the axes. Point2D[] ortogonalSquareVertices = { new Point2D.Double(-widthHalf, -widthHalf), new Point2D.Double(-widthHalf, widthHalf), new Point2D.Double(widthHalf, widthHalf), new Point2D.Double(widthHalf, -widthHalf) }; Point2D[] rotatedSquareVertices = getRotatedSquareVertices(ortogonalSquareVertices, rotationAngle, squareCenter); Subpath square = new Subpath(); square.addSegment(new Line(rotatedSquareVertices[0], rotatedSquareVertices[1])); square.addSegment(new Line(rotatedSquareVertices[1], rotatedSquareVertices[2])); square.addSegment(new Line(rotatedSquareVertices[2], rotatedSquareVertices[3])); square.addSegment(new Line(rotatedSquareVertices[3], rotatedSquareVertices[0])); return square; } private static Point2D[] getRotatedSquareVertices(Point2D[] orthogonalSquareVertices, double angle, Point2D squareCenter) { Point2D[] rotatedSquareVertices = new Point2D[orthogonalSquareVertices.length]; AffineTransform.getRotateInstance(angle). transform(orthogonalSquareVertices, 0, rotatedSquareVertices, 0, rotatedSquareVertices.length); AffineTransform.getTranslateInstance(squareCenter.getX(), squareCenter.getY()). transform(rotatedSquareVertices, 0, rotatedSquareVertices, 0, orthogonalSquareVertices.length); return rotatedSquareVertices; } /** * Adds all subpaths of the path to the {@link ClipperOffset} object with one * note: it doesn't add degenerate subpaths. * * @return {@link java.util.List} consisting of all degenerate subpaths of the path. */ private static List<Subpath> addPath(ClipperOffset offset, Path path, JoinType joinType, EndType endType) { List<Subpath> degenerateSubpaths = new ArrayList<Subpath>(); for (Subpath subpath : path.getSubpaths()) { if (subpath.isDegenerate()) { degenerateSubpaths.add(subpath); continue; } if (!subpath.isSinglePointClosed() && !subpath.isSinglePointOpen()) { EndType et; if (subpath.isClosed()) { // Offsetting is never used for path being filled et = EndType.CLOSED_LINE; } else { et = endType; } List<Point2D> linearApproxPoints = subpath.getPiecewiseLinearApproximation(); offset.addPath(convertToIntPoints(linearApproxPoints), joinType, et); } } return degenerateSubpaths; } private static BezierCurve[] approximateCircle(Point2D center, double radius) { // The circle is split into 4 sectors. Arc of each sector // is approximated with bezier curve separately. BezierCurve[] approximation = new BezierCurve[4]; double x = center.getX(); double y = center.getY(); approximation[0] = new BezierCurve(Arrays.asList( (Point2D) new Point2D.Double(x, y + radius), new Point2D.Double(x + radius * circleApproximationConst, y + radius), new Point2D.Double(x + radius, y + radius * circleApproximationConst), new Point2D.Double(x + radius, y))); approximation[1] = new BezierCurve(Arrays.asList( (Point2D) new Point2D.Double(x + radius, y), new Point2D.Double(x + radius, y - radius * circleApproximationConst), new Point2D.Double(x + radius * circleApproximationConst, y - radius), new Point2D.Double(x, y - radius))); approximation[2] = new BezierCurve(Arrays.asList( (Point2D) new Point2D.Double(x, y - radius), new Point2D.Double(x - radius * circleApproximationConst, y - radius), new Point2D.Double(x - radius, y - radius * circleApproximationConst), new Point2D.Double(x - radius, y))); approximation[3] = new BezierCurve(Arrays.asList( (Point2D) new Point2D.Double(x - radius, y), new Point2D.Double(x - radius, y + radius * circleApproximationConst), new Point2D.Double(x - radius * circleApproximationConst, y + radius), new Point2D.Double(x, y + radius))); return approximation; } private static void addPath(Clipper clipper, Path path) { for (Subpath subpath : path.getSubpaths()) { if (!subpath.isSinglePointClosed() && !subpath.isSinglePointOpen()) { List<Point2D> linearApproxPoints = subpath.getPiecewiseLinearApproximation(); clipper.addPath(convertToIntPoints(linearApproxPoints), PolyType.SUBJECT, subpath.isClosed()); } } } private static void addRect(Clipper clipper, Point2D[] rectVertices, PolyType polyType) { clipper.addPath(convertToIntPoints(new ArrayList<Point2D>(Arrays.asList(rectVertices))), polyType, true); } private static com.itextpdf.text.pdf.parser.clipper.Path convertToIntPoints(List<Point2D> points) { List<LongPoint> convertedPoints = new ArrayList<LongPoint>(points.size()); for (Point2D point : points) { convertedPoints.add(new LongPoint(PdfCleanUpProcessor.floatMultiplier * point.getX(), PdfCleanUpProcessor.floatMultiplier * point.getY())); } return new com.itextpdf.text.pdf.parser.clipper.Path(convertedPoints); } private static List<Point2D> convertToFloatPoints(List<LongPoint> points) { List<Point2D> convertedPoints = new ArrayList<Point2D>(points.size()); for (LongPoint point : points) { convertedPoints.add(new Point2D.Float((float) (point.getX() / PdfCleanUpProcessor.floatMultiplier), (float) (point.getY() / PdfCleanUpProcessor.floatMultiplier))); } return convertedPoints; } private static Path convertToPath(PolyTree result) { Path path = new Path(); PolyNode node = result.getFirst(); while (node != null) { addContour(path, node.getContour(), !node.isOpen()); node = node.getNext(); } return path; } private static void addContour(Path path, List<LongPoint> contour, Boolean close) { List<Point2D> floatContour = convertToFloatPoints(contour); Iterator<Point2D> iter = floatContour.iterator(); Point2D point = iter.next(); path.moveTo((float) point.getX(), (float) point.getY()); while (iter.hasNext()) { point = iter.next(); path.lineTo((float) point.getX(), (float) point.getY()); } if (close) { path.closeSubpath(); } } private Point2D[] getVertices(Rectangle rect) { Point2D[] points = { new Point2D.Double(rect.getLeft(), rect.getBottom()), new Point2D.Double(rect.getRight(), rect.getBottom()), new Point2D.Double(rect.getRight(), rect.getTop()), new Point2D.Double(rect.getLeft(), rect.getTop()) }; return points; } private boolean intersect(Point2D[] rect1, Point2D[] rect2) { Clipper clipper = new DefaultClipper(); addRect(clipper, rect1, PolyType.SUBJECT); addRect(clipper, rect2, PolyType.CLIP); Paths paths = new Paths(); clipper.execute(ClipType.INTERSECTION, paths, PolyFillType.NON_ZERO, PolyFillType.NON_ZERO); return !paths.isEmpty(); } /** * @return Image boundary rectangle in device space. */ private Rectangle calcImageRect(ImageRenderInfo renderInfo) { Matrix ctm = renderInfo.getImageCTM(); if (ctm == null) { return null; } Point2D[] points = transformPoints(ctm, false, new Point(0, 0), new Point(0, 1), new Point(1, 0), new Point(1, 1)); return getRectangle(points[0], points[1], points[2], points[3]); } /** * @return null if the intersection is empty, {@link com.itextpdf.text.Rectangle} representing intersection otherwise */ private Rectangle intersection(Rectangle rect1, Rectangle rect2) { com.itextpdf.awt.geom.Rectangle awtRect1 = new com.itextpdf.awt.geom.Rectangle(rect1); com.itextpdf.awt.geom.Rectangle awtRect2 = new com.itextpdf.awt.geom.Rectangle(rect2); com.itextpdf.awt.geom.Rectangle awtIntersection = awtRect1.intersection(awtRect2); return awtIntersection.isEmpty() ? null : new Rectangle(awtIntersection); } /** * Transforms the given Rectangle into the image coordinate system which is [0,1]x[0,1] by default */ private Rectangle transformIntersection(Matrix imageCTM, Rectangle rect) { Point2D[] points = transformPoints(imageCTM, true, new Point(rect.getLeft(), rect.getBottom()), new Point(rect.getLeft(), rect.getTop()), new Point(rect.getRight(), rect.getBottom()), new Point(rect.getRight(), rect.getTop())); return getRectangle(points[0], points[1], points[2], points[3]); } /** * Constructs Rectangle object on the given points */ private Rectangle getRectangle(Point2D p1, Point2D p2, Point2D p3, Point2D p4) { List<Double> xs = Arrays.asList(p1.getX(), p2.getX(), p3.getX(), p4.getX()); List<Double> ys = Arrays.asList(p1.getY(), p2.getY(), p3.getY(), p4.getY()); double left = Collections.min(xs); double bottom = Collections.min(ys); double right = Collections.max(xs); double top = Collections.max(ys); return new Rectangle((float) left, (float) bottom, (float) right, (float) top); } private static Path applyDashPattern(Path path, LineDashPattern lineDashPattern) { Set<Integer> modifiedSubpaths = new HashSet<Integer>(path.replaceCloseWithLine()); Path dashedPath = new Path(); int currentSubpath = 0; for (Subpath subpath : path.getSubpaths()) { List<Point2D> subpathApprox = subpath.getPiecewiseLinearApproximation(); if (subpathApprox.size() > 1) { dashedPath.moveTo((float) subpathApprox.get(0).getX(), (float) subpathApprox.get(0).getY()); float remainingDist = 0; boolean remainingIsGap = false; for (int i = 1; i < subpathApprox.size(); ++i) { Point2D nextPoint = null; if (remainingDist != 0) { nextPoint = getNextPoint(subpathApprox.get(i - 1), subpathApprox.get(i), remainingDist); remainingDist = applyDash(dashedPath, subpathApprox.get(i - 1), subpathApprox.get(i), nextPoint, remainingIsGap); } while (Float.compare(remainingDist, 0) == 0 && !dashedPath.getCurrentPoint().equals(subpathApprox.get(i))) { LineDashPattern.DashArrayElem currentElem = lineDashPattern.next(); nextPoint = getNextPoint(nextPoint != null ? nextPoint : subpathApprox.get(i - 1), subpathApprox.get(i), currentElem.getVal()); remainingDist = applyDash(dashedPath, subpathApprox.get(i - 1), subpathApprox.get(i), nextPoint, currentElem.isGap()); remainingIsGap = currentElem.isGap(); } } // If true, then the line closing the subpath was explicitly added (see Path.ReplaceCloseWithLine). // This causes a loss of a visual effect of line join style parameter, so in this clause // we simply add overlapping dash (or gap, no matter), which continues the last dash and equals to // the first dash (or gap) of the path. if (modifiedSubpaths.contains(currentSubpath)) { lineDashPattern.reset(); LineDashPattern.DashArrayElem currentElem = lineDashPattern.next(); Point2D nextPoint = getNextPoint(subpathApprox.get(0), subpathApprox.get(1), currentElem.getVal()); applyDash(dashedPath, subpathApprox.get(0), subpathApprox.get(1), nextPoint, currentElem.isGap()); } } // According to PDF spec. line dash pattern should be restarted for each new subpath. lineDashPattern.reset(); ++currentSubpath; } return dashedPath; } private static Point2D getNextPoint(Point2D segStart, Point2D segEnd, float dist) { Point2D vector = componentwiseDiff(segEnd, segStart); Point2D unitVector = getUnitVector(vector); return new Point2D.Float((float) (segStart.getX() + dist * unitVector.getX()), (float) (segStart.getY() + dist * unitVector.getY())); } private static Point2D componentwiseDiff(Point2D minuend, Point2D subtrahend) { return new Point2D.Float((float) (minuend.getX() - subtrahend.getX()), (float) (minuend.getY() - subtrahend.getY())); } private static Point2D getUnitVector(Point2D vector) { double vectorLength = getVectorEuclideanNorm(vector); return new Point2D.Float((float) (vector.getX() / vectorLength), (float) (vector.getY() / vectorLength)); } private static double getVectorEuclideanNorm(Point2D vector) { return vector.distance(0, 0); } private static float applyDash(Path dashedPath, Point2D segStart, Point2D segEnd, Point2D dashTo, boolean isGap) { float remainingDist = 0; if (!liesOnSegment(segStart, segEnd, dashTo)) { remainingDist = (float) dashTo.distance(segEnd); dashTo = segEnd; } if (isGap) { dashedPath.moveTo((float) dashTo.getX(), (float) dashTo.getY()); } else { dashedPath.lineTo((float) dashTo.getX(), (float) dashTo.getY()); } return remainingDist; } private static boolean liesOnSegment(Point2D segStart, Point2D segEnd, Point2D point) { return point.getX() >= Math.min(segStart.getX(), segEnd.getX()) && point.getX() <= Math.max(segStart.getX(), segEnd.getX()) && point.getY() >= Math.min(segStart.getY(), segEnd.getY()) && point.getY() <= Math.max(segStart.getY(), segEnd.getY()); } private Point2D[] transformPoints(Matrix transormationMatrix, boolean inverse, Point2D... points) { AffineTransform t = new AffineTransform(transormationMatrix.get(Matrix.I11), transormationMatrix.get(Matrix.I12), transormationMatrix.get(Matrix.I21), transormationMatrix.get(Matrix.I22), transormationMatrix.get(Matrix.I31), transormationMatrix.get(Matrix.I32)); Point2D[] transformed = new Point2D[points.length]; if (inverse) { try { t = t.createInverse(); } catch (NoninvertibleTransformException e) { throw new RuntimeException(e); } } t.transform(points, 0, transformed, 0, points.length); return transformed; } // Constants from the standard line representation: Ax+By+C private static class StandardLine { float A; float B; float C; StandardLine(Point2D p1, Point2D p2) { A = (float) (p2.getY() - p1.getY()); B = (float) (p1.getX() - p2.getX()); C = (float) (p1.getY() * (-B) - p1.getX() * A); } float getSlope() { if (B == 0) { return Float.POSITIVE_INFINITY; } return -A / B; } boolean contains(Point2D point) { return Float.compare(Math.abs(A * (float) point.getX() + B * (float) point.getY() + C), 0.1f) < 0; } } }